using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace JelleDruyts.Mollom.Client
{
    /// <summary>
    /// Provides access to an XML-RPC server.
    /// </summary>
    internal sealed class XmlRpcServer
    {
        #region Fields

        /// <summary>
        /// The regular expression used to find field names and values in XML-RPC structs.
        /// </summary>
        private static readonly Regex StructureFieldExpression = new Regex("<member>\\s*<name>(?<name>.*?)</name>\\s*<value>\\s*<(?<type>.*?)>(?<value>.*?)</(?<type>.*?)>\\s*</value>\\s*</member>", RegexOptions.Compiled);

        /// <summary>
        /// The regular expression used to find field values in XML-RPC structs.
        /// </summary>
        private static readonly Regex ListFieldExpression = new Regex("<value>\\s*<(?<type>.*?)>(?<value>.*?)</(?<type>.*?)>\\s*</value>", RegexOptions.Compiled);

        /// <summary>
        /// The regular expression used to find the return value in an XML-RPC response.
        /// </summary>
        private static readonly Regex ReturnValueFieldExpression = new Regex("<methodResponse>\\s*<params>\\s*<param>\\s*<value>\\s*<(?<type>.*?)>(?<value>.*?)</(?<type>.*?)>\\s*</value>\\s*</param>\\s*</params>\\s*</methodResponse>", RegexOptions.Compiled);

        /// <summary>
        /// The HTTP User-Agent header to be sent to the server.
        /// </summary>
        private readonly string userAgent;

        /// <summary>
        /// The maximum number of times to retry a failed operation. The default is 5.
        /// </summary>
        private readonly int maxRetryCount;

        #endregion

        #region Url Property

        /// <summary>
        /// The URL of the XML-RPC server.
        /// </summary>
        private string url;

        /// <summary>
        /// Gets or sets the URL of the XML-RPC server.
        /// </summary>
        public string Url
        {
            get
            {
                return this.url;
            }
            set
            {
                this.url = value;
            }
        }

        #endregion

        #region OperationFailed Event

        /// <summary>
        /// Occurs when an XML-RPC operation failed.
        /// </summary>
        public event EventHandler<OperationFailedEventArgs> OperationFailed;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="XmlRpcServer"/> class.
        /// </summary>
        /// <param name="url">The URL of the XML-RPC server.</param>
        /// <param name="userAgent">The HTTP User-Agent header to be sent to the server.</param>
        public XmlRpcServer(string url, string userAgent)
            : this(url, userAgent, 5)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="XmlRpcServer"/> class.
        /// </summary>
        /// <param name="url">The URL of the XML-RPC server.</param>
        /// <param name="userAgent">The HTTP User-Agent header to be sent to the server.</param>
        /// <param name="maxRetryCount">The maximum number of times to retry a failed operation. The default is 5.</param>
        public XmlRpcServer(string url, string userAgent, int maxRetryCount)
        {
            this.url = url;
            this.userAgent = userAgent;
            this.maxRetryCount = maxRetryCount;
        }

        #endregion

        #region CallMethod

        /// <summary>
        /// Calls the given XML-RPC method.
        /// </summary>
        /// <param name="methodName">The name of the method to call.</param>
        /// <param name="parameters">The parameters to be passed to the method.</param>
        /// <returns>The boolean value inside the response message.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        /// <exception cref="XmlRpcException">The XML-RPC response did not contain a valid return value.</exception>
        public bool CallMethodAsBool(string methodName, IDictionary<string, string> parameters)
        {
            string response = CallMethod(methodName, parameters);
            string returnValue = GetReturnValue(response);
            return (returnValue == "1");
        }

        /// <summary>
        /// Calls the given XML-RPC method.
        /// </summary>
        /// <param name="methodName">The name of the method to call.</param>
        /// <param name="parameters">The parameters to be passed to the method.</param>
        /// <returns>The integer value inside the response message.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        /// <exception cref="XmlRpcException">The XML-RPC response did not contain a valid return value.</exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        public int CallMethodAsInt(string methodName, IDictionary<string, string> parameters)
        {
            string response = CallMethod(methodName, parameters);
            string returnValue = GetReturnValue(response);
            return int.Parse(returnValue, CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Calls the given XML-RPC method.
        /// </summary>
        /// <param name="methodName">The name of the method to call.</param>
        /// <param name="parameters">The parameters to be passed to the method.</param>
        /// <returns>A list with all items found in the response message's structure.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        public IList<string> CallMethodAsList(string methodName, IDictionary<string, string> parameters)
        {
            string response = CallMethod(methodName, parameters);
            return GetList(response);
        }

        /// <summary>
        /// Calls the given XML-RPC method.
        /// </summary>
        /// <param name="methodName">The name of the method to call.</param>
        /// <param name="parameters">The parameters to be passed to the method.</param>
        /// <returns>A dictionary with all key/value pairs found in the response message's structure.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        public IDictionary<string, string> CallMethodAsDictionary(string methodName, IDictionary<string, string> parameters)
        {
            string response = CallMethod(methodName, parameters);
            return GetDictionary(response);
        }

        /// <summary>
        /// Calls the given XML-RPC method.
        /// </summary>
        /// <param name="methodName">The name of the method to call.</param>
        /// <param name="parameters">The parameters to be passed to the method.</param>
        /// <returns>The response message.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        public string CallMethod(string methodName, IDictionary<string, string> parameters)
        {
            // Validate arguments.
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentException("The method name is required.", "methodName");
            }
            if (parameters == null)
            {
                parameters = new Dictionary<string, string>();
            }

            // Create the XML-RPC message to post.
            StringBuilder message = new StringBuilder();
            message.Append("<?xml version=\"1.0\"?>");
            message.Append("<methodCall>");
            message.AppendFormat(CultureInfo.InvariantCulture, "<methodName>{0}</methodName>", methodName);
            message.Append("<params><param><value><struct>");
            foreach (KeyValuePair<string, string> parameter in parameters)
            {
                if (!string.IsNullOrEmpty(parameter.Key) && !string.IsNullOrEmpty(parameter.Value))
                {
                    message.AppendFormat(CultureInfo.InvariantCulture, "<member><name>{0}</name><value><string>{1}</string></value></member>", parameter.Key, parameter.Value);
                }
            }
            message.Append("</struct></value></param></params>");
            message.Append("</methodCall>");

            // Perform the HTTP-POST and return the results.
            return PerformPost(message.ToString());
        }

        #endregion

        #region Helper Methods

        /// <summary>
        /// Parses the given XML-RPC response and returns the return value
        /// </summary>
        /// <param name="response">The XML-RPC response that contains a return value.</param>
        /// <returns>The return value found in the response.</returns>
        /// <exception cref="XmlRpcException">The XML-RPC response did not contain a valid return value.</exception>
        private static string GetReturnValue(string response)
        {
            Match fieldMatch = ReturnValueFieldExpression.Match(response);
            if (fieldMatch.Success && fieldMatch.Groups["value"].Success)
            {
                return HttpUtility.HtmlDecode(fieldMatch.Groups["value"].Value);
            }
            else
            {
                throw new XmlRpcException("The XML-RPC response did not contain a valid return value.");
            }
        }

        /// <summary>
        /// Parses the given XML-RPC message and returns all list items in the XML-RPC structure.
        /// </summary>
        /// <param name="message">The XML-RPC message that contains a structure with list items.</param>
        /// <returns>A list with all items found in the message's structure.</returns>
        private static IList<string> GetList(string message)
        {
            List<string> data = new List<string>();
            foreach (Match fieldMatch in ListFieldExpression.Matches(message))
            {
                if (fieldMatch.Success && fieldMatch.Groups["value"].Success)
                {
                    data.Add(HttpUtility.HtmlDecode(fieldMatch.Groups["value"].Value));
                }
            }
            return data;
        }

        /// <summary>
        /// Parses the given XML-RPC message and returns all key/value pairs in the XML-RPC structure.
        /// </summary>
        /// <param name="message">The XML-RPC message that contains a structure with key/value pairs.</param>
        /// <returns>A dictionary with all key/value pairs found in the message's structure.</returns>
        private static IDictionary<string, string> GetDictionary(string message)
        {
            Dictionary<string, string> data = new Dictionary<string, string>();
            foreach (Match fieldMatch in StructureFieldExpression.Matches(message))
            {
                if (fieldMatch.Success && fieldMatch.Groups["name"].Success && fieldMatch.Groups["value"].Success)
                {
                    data.Add(HttpUtility.HtmlDecode(fieldMatch.Groups["name"].Value), HttpUtility.HtmlDecode(fieldMatch.Groups["value"].Value));
                }
            }
            return data;
        }

        /// <summary>
        /// Performs an HTTP POST to an XML-RPC server.
        /// </summary>
        /// <param name="postData">The data to post to the server.</param>
        /// <returns>The response message.</returns>
        /// <exception cref="XmlRpcFaultException">The XML-RPC response contained an XML-RPC fault.</exception>
        private string PerformPost(string postData)
        {
            bool shouldRetry = true;
            int retryCount = 0;
            string responseMessage = null;

            while (shouldRetry)
            {
                shouldRetry = false;

                try
                {
                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.url);
                    if (!string.IsNullOrEmpty(this.userAgent))
                    {
                        request.UserAgent = this.userAgent;
                    }
                    request.ContentType = "text/xml";
                    request.Method = "POST";

                    // Convert the input to a byte array and get the length of the payload.
                    byte[] requestBytes = Encoding.ASCII.GetBytes(postData.ToString());
                    request.ContentLength = requestBytes.Length;

                    // Write the bytes.
                    using (Stream outputStream = request.GetRequestStream())
                    {
                        outputStream.Write(requestBytes, 0, requestBytes.Length);
                    }

                    // Get the response and read from the stream.
                    HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                    using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                    {
                        responseMessage = reader.ReadToEnd();
                    }

                    // Check if the response is an XML-RPC fault.
                    if (responseMessage.IndexOf("<fault>", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        IDictionary<string, string> faultInformation = GetDictionary(responseMessage);
                        string faultCodeText = faultInformation["faultCode"];
                        int faultCode = int.Parse(faultCodeText, CultureInfo.InvariantCulture);
                        string faultString = faultInformation["faultString"];
                        throw new XmlRpcFaultException(string.Format(CultureInfo.CurrentCulture, "An error occurred while calling the server: [{0}] {1}", faultCode, faultString), faultString, faultCode);
                    }
                }
                catch (Exception exc)
                {
                    // Allow context-based retries if there was an exception.
                    // Prevent unlimited retries when the same call always fails.
                    if (retryCount >= this.maxRetryCount)
                    {
                        throw;
                    }

                    // Check with event handlers if the operation should be retried.
                    if (this.OperationFailed != null)
                    {
                        OperationFailedEventArgs e = new OperationFailedEventArgs(exc, false, retryCount);
                        this.OperationFailed(this, e);
                        shouldRetry = e.ShouldRetry;
                    }

                    // Bail out if we shouldn't retry, otherwise keep going and increment the retry counter.
                    if (!shouldRetry)
                    {
                        throw;
                    }
                    else
                    {
                        retryCount++;
                    }
                }
            }

            return responseMessage;
        }

        #endregion
    }
}